{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Simple electricity market examples\n",
    "\n",
    "This example gradually builds up more and more complicated energy-only electricity markets in PyPSA, starting from a single bidding zone, going up to multiple bidding zones connected with transmission (NTCs) along with variable renewables and storage."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Preliminaries\n",
    "\n",
    "Here libraries are imported and data is defined."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pypsa, numpy as np"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#marginal costs in EUR/MWh\n",
    "marginal_costs = {\"Wind\" : 0,\n",
    "                  \"Hydro\" : 0,\n",
    "                  \"Coal\" : 30,\n",
    "                  \"Gas\" : 60,\n",
    "                  \"Oil\" : 80}\n",
    "\n",
    "#power plant capacities (nominal powers in MW) in each country (not necessarily realistic)\n",
    "power_plant_p_nom = {\"South Africa\" : {\"Coal\" : 35000,\n",
    "                                       \"Wind\" : 3000,\n",
    "                                       \"Gas\" : 8000,\n",
    "                                       \"Oil\" : 2000\n",
    "                                      },\n",
    "                     \"Mozambique\" : {\"Hydro\" : 1200,\n",
    "                                    },\n",
    "                     \"Swaziland\" : {\"Hydro\" : 600,\n",
    "                                    },\n",
    "                    }\n",
    "\n",
    "#transmission capacities in MW (not necessarily realistic)\n",
    "transmission = {\"South Africa\" : {\"Mozambique\" : 500,\n",
    "                                  \"Swaziland\" : 250},\n",
    "                \"Mozambique\" : {\"Swaziland\" : 100}}\n",
    "\n",
    "#country electrical loads in MW (not necessarily realistic)\n",
    "loads = {\"South Africa\" : 42000,\n",
    "         \"Mozambique\" : 650,\n",
    "         \"Swaziland\" : 250}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Single bidding zone with fixed load, one period\n",
    "\n",
    "In this example we consider a single market bidding zone, South Africa.\n",
    "\n",
    "The inelastic load has essentially infinite marginal utility (or higher than the marginal cost of any generator)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "country = \"South Africa\"\n",
    "\n",
    "network = pypsa.Network()\n",
    "\n",
    "network.add(\"Bus\",country)\n",
    "\n",
    "for tech in power_plant_p_nom[country]:\n",
    "    network.add(\"Generator\",\n",
    "                \"{} {}\".format(country,tech),\n",
    "                bus=country,\n",
    "                p_nom=power_plant_p_nom[country][tech],\n",
    "                marginal_cost=marginal_costs[tech])\n",
    "\n",
    "\n",
    "network.add(\"Load\",\n",
    "            \"{} load\".format(country),\n",
    "            bus=country,\n",
    "            p_set=loads[country])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#Run optimisation to determine market dispatch\n",
    "network.lopf()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#print the load active power (P) consumption\n",
    "network.loads_t.p"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#print the generator active power (P) dispatch\n",
    "network.generators_t.p"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#print the clearing price (corresponding to gas)\n",
    "network.buses_t.marginal_price"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Two bidding zones connected by transmission, one period\n",
    "\n",
    "In this example we have bidirectional transmission capacity between two bidding zones. The power transfer is treated as controllable (like an A/NTC (Available/Net Transfer Capacity) or HVDC line). Note that in the physical grid, power flows passively according to the network impedances."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "network = pypsa.Network()\n",
    "\n",
    "countries = [\"Mozambique\", \"South Africa\"]\n",
    "\n",
    "for country in countries:\n",
    "    \n",
    "    network.add(\"Bus\",country)\n",
    "\n",
    "    for tech in power_plant_p_nom[country]:\n",
    "        network.add(\"Generator\",\n",
    "                    \"{} {}\".format(country,tech),\n",
    "                    bus=country,\n",
    "                    p_nom=power_plant_p_nom[country][tech],\n",
    "                    marginal_cost=marginal_costs[tech])\n",
    "\n",
    "\n",
    "    network.add(\"Load\",\n",
    "                \"{} load\".format(country),\n",
    "                bus=country,\n",
    "                p_set=loads[country])\n",
    "    \n",
    "    #add transmission as controllable Link\n",
    "    if country not in transmission:\n",
    "        continue\n",
    "    \n",
    "    for other_country in countries:\n",
    "        if other_country not in transmission[country]:\n",
    "            continue\n",
    "        \n",
    "        #NB: Link is by default unidirectional, so have to set p_min_pu = -1\n",
    "        #to allow bidirectional (i.e. also negative) flow\n",
    "        network.add(\"Link\",\n",
    "                   \"{} - {} link\".format(country, other_country),\n",
    "                   bus0=country,\n",
    "                   bus1=other_country,\n",
    "                   p_nom=transmission[country][other_country],\n",
    "                   p_min_pu=-1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "network.lopf()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "network.loads_t.p"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "network.generators_t.p"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "network.links_t.p0"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#print the clearing price (corresponding to water in Mozambique and gas in SA)\n",
    "network.buses_t.marginal_price"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#link shadow prices\n",
    "network.links_t.mu_lower"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Three bidding zones connected by transmission, one period\n",
    "\n",
    "In this example we have bidirectional transmission capacity between three bidding zones. The power transfer is treated as controllable (like an A/NTC (Available/Net Transfer Capacity) or HVDC line). Note that in the physical grid, power flows passively according to the network impedances."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "network = pypsa.Network()\n",
    "\n",
    "countries = [\"Swaziland\", \"Mozambique\", \"South Africa\"]\n",
    "\n",
    "for country in countries:\n",
    "    \n",
    "    network.add(\"Bus\",country)\n",
    "\n",
    "    for tech in power_plant_p_nom[country]:\n",
    "        network.add(\"Generator\",\n",
    "                    \"{} {}\".format(country,tech),\n",
    "                    bus=country,\n",
    "                    p_nom=power_plant_p_nom[country][tech],\n",
    "                    marginal_cost=marginal_costs[tech])\n",
    "\n",
    "\n",
    "    network.add(\"Load\",\n",
    "                \"{} load\".format(country),\n",
    "                bus=country,\n",
    "                p_set=loads[country])\n",
    "    \n",
    "    #add transmission as controllable Link\n",
    "    if country not in transmission:\n",
    "        continue\n",
    "    \n",
    "    for other_country in countries:\n",
    "        if other_country not in transmission[country]:\n",
    "            continue\n",
    "        \n",
    "        #NB: Link is by default unidirectional, so have to set p_min_pu = -1\n",
    "        #to allow bidirectional (i.e. also negative) flow\n",
    "        network.add(\"Link\",\n",
    "                   \"{} - {} link\".format(country, other_country),\n",
    "                   bus0=country,\n",
    "                   bus1=other_country,\n",
    "                   p_nom=transmission[country][other_country],\n",
    "                   p_min_pu=-1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "network.lopf()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "network.loads_t.p"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "network.generators_t.p"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "network.links_t.p0"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#print the clearing price (corresponding to hydro in S and M, and gas in SA)\n",
    "network.buses_t.marginal_price"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#link shadow prices\n",
    "network.links_t.mu_lower"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Single bidding zone with price-sensitive industrial load, one period\n",
    "\n",
    "In this example we consider a single market bidding zone, South Africa.\n",
    "\n",
    "Now there is a large industrial load with a marginal utility which is low enough to interact with the generation marginal cost."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "country = \"South Africa\"\n",
    "\n",
    "network = pypsa.Network()\n",
    "\n",
    "network.add(\"Bus\",country)\n",
    "\n",
    "for tech in power_plant_p_nom[country]:\n",
    "    network.add(\"Generator\",\n",
    "                \"{} {}\".format(country,tech),\n",
    "                bus=country,\n",
    "                p_nom=power_plant_p_nom[country][tech],\n",
    "                marginal_cost=marginal_costs[tech])\n",
    "\n",
    "#standard high marginal utility consumers\n",
    "network.add(\"Load\",\n",
    "            \"{} load\".format(country),\n",
    "            bus=country,\n",
    "            p_set=loads[country])\n",
    "\n",
    "#add an industrial load as a dummy negative-dispatch generator with marginal utility of 70 EUR/MWh for 8000 MW\n",
    "network.add(\"Generator\",\n",
    "            \"{} industrial load\".format(country),\n",
    "            bus=country,\n",
    "            p_max_pu=0,\n",
    "            p_min_pu=-1,\n",
    "            p_nom=8000,\n",
    "            marginal_cost=70)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "network.lopf()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "network.loads_t.p"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#NB only half of industrial load is served, because this maxes out \n",
    "#Gas. Oil is too expensive with a marginal cost of 80 EUR/MWh\n",
    "network.generators_t.p"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "network.buses_t.marginal_price"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Single bidding zone with fixed load, several periods\n",
    "\n",
    "In this example we consider a single market bidding zone, South Africa.\n",
    "\n",
    "We consider multiple time periods (labelled [0,1,2,3]) to represent variable wind generation."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "country = \"South Africa\"\n",
    "\n",
    "network = pypsa.Network()\n",
    "\n",
    "#snapshots labelled by [0,1,2,3]\n",
    "network.set_snapshots(range(4))\n",
    "\n",
    "network.add(\"Bus\",country)\n",
    "\n",
    "#p_max_pu is variable for wind\n",
    "for tech in power_plant_p_nom[country]:\n",
    "    network.add(\"Generator\",\n",
    "                \"{} {}\".format(country,tech),\n",
    "                bus=country,\n",
    "                p_nom=power_plant_p_nom[country][tech],\n",
    "                marginal_cost=marginal_costs[tech],\n",
    "                p_max_pu=([0.3,0.6,0.4,0.5] if tech == \"Wind\" else 1),\n",
    "                )\n",
    "\n",
    "#load which varies over the snapshots\n",
    "network.add(\"Load\",\n",
    "            \"{} load\".format(country),\n",
    "            bus=country,\n",
    "            p_set=loads[country] + np.array([0,1000,3000,4000]))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#specify that we consider all snapshots\n",
    "network.lopf(network.snapshots)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "network.loads_t.p"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "network.generators_t.p"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "network.buses_t.marginal_price"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Single bidding zone with fixed load and storage, several periods\n",
    "\n",
    "In this example we consider a single market bidding zone, South Africa.\n",
    "\n",
    "We consider multiple time periods (labelled [0,1,2,3]) to represent variable wind generation. Storage is allowed to do price arbitrage to reduce oil consumption."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "country = \"South Africa\"\n",
    "\n",
    "network = pypsa.Network()\n",
    "\n",
    "#snapshots labelled by [0,1,2,3]\n",
    "network.set_snapshots(range(4))\n",
    "\n",
    "network.add(\"Bus\",country)\n",
    "\n",
    "#p_max_pu is variable for wind\n",
    "for tech in power_plant_p_nom[country]:\n",
    "    network.add(\"Generator\",\n",
    "                \"{} {}\".format(country,tech),\n",
    "                bus=country,\n",
    "                p_nom=power_plant_p_nom[country][tech],\n",
    "                marginal_cost=marginal_costs[tech],\n",
    "                p_max_pu=([0.3,0.6,0.4,0.5] if tech == \"Wind\" else 1),\n",
    "                )\n",
    "\n",
    "#load which varies over the snapshots\n",
    "network.add(\"Load\",\n",
    "            \"{} load\".format(country),\n",
    "            bus=country,\n",
    "            p_set=loads[country] + np.array([0,1000,3000,4000]))\n",
    "\n",
    "#storage unit to do price arbitrage\n",
    "network.add(\"StorageUnit\",\n",
    "            \"{} pumped hydro\".format(country),\n",
    "            bus=country,\n",
    "            p_nom=1000,\n",
    "            max_hours=6, #energy storage in terms of hours at full power\n",
    "           )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "network.lopf(network.snapshots)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "network.loads_t.p"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "network.generators_t.p"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "network.storage_units_t.p"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "network.storage_units_t.state_of_charge"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "network.buses_t.marginal_price"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
